package sg.edu.nus.iss.jass.datastore;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import sg.edu.nus.iss.jass.datastore.exception.DataStoreException;

/**
 * Abstract class for all file storage used in data store
 * 
 * @author Wang Dayong
 *
 */
public abstract class AbstractFileStorage {

	private static String PATH_SEP = System.getProperty("file.separator");

	/**
	 * Global cache to cache object in byte array format
	 */
	private static Map<String, byte[]> cache = new HashMap<String, byte[]>();

	/**
	 * The root path to store files
	 */
	private String rootPath;

	/**
	 * indicator of cache on or off, default value is on
	 */
	private boolean isCacheOn = true;

	/**
	 * indicator of flush instantly on or off, default value is on
	 */
	private boolean isInstantFlush = true;
	
	
	/**
	 */
	public AbstractFileStorage() {

		super();
	}

	/**
	 * Constructor 
	 * @param rootPath the storage root path
	 * @param isCacheOn whether enable cache, the cache will sync with file on HD
	 */
	public AbstractFileStorage(String rootPath, boolean isCacheOn) {

		super();
		this.rootPath = rootPath;
		this.isCacheOn = isCacheOn;
	}

	/**
	 * 
	 * @param rootPath the storage root path
	 * 	This constructor set cache on as default
	 */
	public AbstractFileStorage(String rootPath) {

		this(rootPath, true);
	}

	/**
	 * <p>save object to specified file name under root folder</p> 
	 * if cache is on and flush immediately is on, the list of objects will be written to file and cache, 
	 * if cache is off, the list of objects will be written to file directly, 
	 * else the list of object will only be written to cache
	 * 
	 * @param fileName
	 *            file name under root folder is to save to
	 * @param obj
	 *            list of objects need to save
	 * @throws DataStoreException
	 * 		Encountering any error while saving data
	 */
	public void save(String fileName, List<? extends BaseDTO> obj) throws DataStoreException {

		try {
			if (isCacheOn) {
				// get the byte array of objects, and write to cache
				byte[] content = getBytesFromObject(obj);
				cache.put(fileName, content);
				// if instant flush is on, will write to file
				if (isInstantFlush) {
					saveBytes2file(fileName, content);
				}
			} else {
				// if cache is off, write to file directlly
				writeObject2File(fileName, obj);
			}

		} catch (IOException e) {
			throw new DataStoreException(e);
		}

	}

	/**
	 * <p>Load objects from specified file under root folder</br> 
	 * if cache is on, cache will be searched first, 
	 * if not in cache, objects will be loaded from specified file
	 * and put into cache. 
	 * if cache is off, objects will be loaded from file directly
	 * 
	 * @param fileName
	 *            file name under root folder is to be loaded
	 * @return Object Objects of list loaded from specified file
	 * @throws DataStoreException
	 * 			Encountering any error while loading data
	 */
	public List<? extends BaseDTO> load(String fileName) throws DataStoreException {

		try {
			// load from cache, if cache is on and contains the content needed
			if (isCacheOn&&cache.containsKey(fileName)) {
				byte[] content = cache.get(fileName);
				return (List<? extends BaseDTO>) loadObjectFromBytes(content);
			}
			// cache is on, and can not get content from cache, get from file
			// and add to cache
			if (isCacheOn) {
				byte[] content = readBytesFromFile(fileName);
				// if content is null, means no data stored in specified file
				if (content==null) {
					return null;
				}
				// put loaded content to cache
				cache.put(fileName, content);
				// convert from byte array to object
				return (List<? extends BaseDTO>) loadObjectFromBytes(content);
			} else {
				// if cache is off, load from file
				return (List<? extends BaseDTO>) loadObjectFromFile(fileName);
			}

		} catch (IOException e) {
			throw new DataStoreException(e);
		} catch (ClassNotFoundException e) {
			throw new DataStoreException(e);
		}

	}


	/**
	 * <p>Flush cache of specified type to storage.</p>
	 * If type is not specified, all caches will be flush to storage.
	 * @param types types is to flush
	 * @throws DataStoreException
	 * 			Encountering error while write data to storage
	 */
	public void flush(String... types) throws DataStoreException {

		if (isCacheOn&&!isInstantFlush) {
			try {
				if (types==null||types.length==0) {
					// TODO:not consider the efficiency, add version check
					// latter
					for (Map.Entry<String, byte[]> entry : cache.entrySet()) {
						this.saveBytes2file(entry.getKey(), entry.getValue());
					}
				} else {
					for (String type : types) {
						this.saveBytes2file(type, cache.get(type));
					}
				}
			} catch (IOException e) {
				throw new DataStoreException(e);
			}
		}

	}

	/**
	 * <P>Write an list of object to specified file under root folder</p>
	 * 
	 * @param fileName The file name that the list of object to be saved to
	 * @param obj the list of object to be stored
	 * @throws IOException
	 */
	protected abstract void writeObject2File(String fileName, List<? extends BaseDTO> obj) throws IOException;

	/**
	 * <p>To load objects from specified file name under root folder.</p>
	 * 
	 * @param fileName the file name to be loaded, the file name should be a full class name
	 * @return Object list of objects loaded from specified file
	 * @throws IOException
	 * @throws ClassNotFoundException
	 * 			The file name is not a class name
	 */
	protected abstract List<? extends BaseDTO> loadObjectFromFile(String fileName) throws IOException, ClassNotFoundException;

	/**
	 * <p>Convert byte array to object</p>
	 * 
	 * @param bytes The content is to convert to list of object
	 * @return List of converted objects
	 * @throws IOException
	 * @throws ClassNotFoundException
	 * 			if the bytes can not convert to appropriate class 
	 */
	protected abstract List<? extends BaseDTO> loadObjectFromBytes(byte[] bytes) throws IOException, ClassNotFoundException;

	/**
	 * <p>Convert object to byte array.</p>
	 * 
	 * @param saveObj the object to be convert to byte array
	 * @return 
	 * 		Converted byte array
	 * @throws IOException
	 * 		Any errors when convert object to byte array.
	 */
	protected abstract byte[] getBytesFromObject(List<? extends BaseDTO> saveObj) throws IOException;

	/**
	 * <p>Clear all contents in cache</p>
	 * 
	 */
	public void clearCache() {

		if (cache!=null) {
			cache.clear();
		}
	}

	/**
	 * set value for isCache on
	 * @param isOn the value to be set
	 */
	public void configCache(boolean isOn) {

		this.isCacheOn = isOn;

	}

	/**
	 * Get the real path of given file name
	 * @param fileName
	 * @return The real path
	 */
	protected String getRealPath(String fileName) {

		return rootPath.concat(PATH_SEP).concat(fileName);
	}

	/**
	 * Set value for variable isInstantFlush
	 * @param instantly The value to be set
	 */
	public void setInstantFlush(boolean instantly) {

		this.isInstantFlush = instantly;
	}

	/**
	 * @return the value of field isInstantFlush
	 */
	public boolean isInstantFlush() {

		return isInstantFlush;
	}


	/**
	 * set storage root path
	 * @param rootPath value to be set
	 */
	public void setRootPath(String rootPath) {

		this.rootPath = rootPath;
	}

	/**
	 * <p>Save byte array to specified path</p>
	 * 
	 * @param fileName the file name under root path
	 * @param bytes the content is to be stored
	 * @throws IOException
	 * 			If any IOException happened when doing IO operation
	 */
	private void saveBytes2file(String fileName, byte[] bytes) throws IOException {

		if (bytes==null||bytes.length==0) {
			return;
		}

		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(new File(getRealPath(fileName)));
			fos.write(bytes);
			fos.flush();
		} finally {
			if (fos!=null)
				fos.close();
		}
	}

	/**
	 * <P>Read file to byte array</P>
	 * 
	 * @param fileName the file under root path is to be loaded
	 * @return byte[] the byte array of content of given file name. If the file is not found, return null
	 * @throws IOException
	 * 			Error when encountering IOException
	 */
	private byte[] readBytesFromFile(String fileName) throws IOException {

		FileInputStream fis = null;
		try {
			fis = new FileInputStream(getRealPath(fileName));
			byte[] buf = new byte[fis.available()];
			fis.read(buf);
			return buf;
		} catch (FileNotFoundException ex) {
			return null;
		} finally {
			if (fis!=null)
				fis.close();
		}

	}

	/**
	 * <p>Copy file from 'orig' to 'copy' under root folder</p>
	 * If cache is on, the related content in the cache will be updated.
	 * 
	 * @param orig the original file under root path
	 * @param copy the copy file name under root path is to used
	 * @throws IOException
	 */
	public void copyFile(String orig, String copy) throws IOException {

		if (isCacheOn) {
			byte[] content = cache.get(orig);
			byte[] newContent = null;
			if (content!=null) {
				newContent = Arrays.copyOf(content, content.length);
			}
			cache.put(copy, newContent);
			// if this content is new one, no need to copy file from HDD
			if (content==null) {
				return;
			}
		}

		// if the original file not exists, return
		File origFile = new File(getRealPath(orig));
		if (!origFile.exists()) {
			return;
		}

		if (isInstantFlush||!isCacheOn) {
			OutputStream os = null;
			InputStream is = null;
			try {

				os = new FileOutputStream(getRealPath(copy));

				is = new FileInputStream(getRealPath(orig));
				byte[] buf = new byte[255];
				int len = 0;
				while ((len = is.read(buf))!=-1) {
					os.write(buf, 0, len);
				}
				os.flush();
			} finally {
				if (is!=null)
					is.close();
				if (os!=null)
					os.close();
			}
		}

	}

	/**
	 * <p>Delete specified file<p>
	 * if cache is on, the related cache will be deleted too.
	 * 
	 * @param delete the file to be deleted under root folder
	 * @throws IOException
	 */
	public void deleteFile(String delete) throws IOException {

		if (isCacheOn) {
			cache.remove(delete);
		}

		if (!isCacheOn||isInstantFlush) {
			File delFile = new File(getRealPath(delete));
			if (delFile.exists()) {
				if (delFile.isDirectory())
					throw new FileNotFoundException("delete file is directory");
				else
					delFile.delete();
			}
		}

	}

}
